package com.yulin.carrunningarcviewdemo; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.Path; import android.graphics.PathMeasure; import android.graphics.RectF; import android.graphics.SweepGradient; import android.os.Handler; import android.os.Parcel; import android.os.Parcelable; import android.util.AttributeSet; import android.util.Log; import android.util.TypedValue; import android.view.View; /** * Created by YuLin on 2017/3/20 0020. * An android custom widget called CarRunningArcView,which has the effect that a car(maybe other icon) * can run on an arc forward and back.And you can transform it to a count down timer view or a loading * view with arc shape and etc. */ public class CarRunningArcView extends View { private static final String TAG = CarRunningArcView.class.getSimpleName(); private static final float DEFAULT_STROKE = 15; private static final float DEFAULT_START_ANGLE = 150; private static final float DEFAULT_SWIPE_ANGLE = 240; private static final int DEFAULT_SLEEP_TIME = 20; private Paint mPaintInside; private Paint mPaintOutside; private int mWidth; private int mHeight; private int mCenterX; private int mCenterY; private float mRadius; private RectF mRectOval; private int mArcInsideColor = Color.WHITE; private float mArcInsideStroke = DEFAULT_STROKE; private float mArcOutSideStroke = DEFAULT_STROKE + 5; private float mStartAngle = DEFAULT_START_ANGLE; private float mSwipeAngle = DEFAULT_SWIPE_ANGLE; private int mSleepTime = DEFAULT_SLEEP_TIME; private int mProgress; private double mRatio; private Bitmap mBitmap; private Matrix mMatrix = null; private Path mPath; private PathMeasure mPathMeasure; private float mBitmapDegrees = 0f; private float mCurrentValue = 0; // 用于纪录当前的位置,取值范围[0,1]映射Path的整个长度 private float[] mPos; // 当前点的实际位置 private float[] mTan; // 当前点的tangent值,用于计算图片所需旋转的角度 private float mPositions[]; private SweepGradient mSweepGradient; private int[] mArcInsideColors; /** * Degree offset for rotating clockwise. */ private int mDegreeClockwise = 0; /** * Degree offset for rotating anticlockwise. */ private int mDegreeAnticlockwise = 180; private boolean isReInitialing = false; private boolean exit = false; private static Handler mHandler = new Handler(); private CarState mCarState = CarState.MOVED_CLOCKWISE; /** * The icon's state. */ public enum CarState { //Moving state. MOVING_CLOCKWISE, MOVED_CLOCKWISE, MOVING_ANTICLOCKWISE, MOVED_ANTICLOCKWISE, //Rotation state. ROTATING_CLOCKWISE, ROTATED_CLOCKWISE, ROTATING_ANTICLOCKWISE, ROTATED_ANTICLOCKWISE } public CarRunningArcView(Context context) { this(context, null); } public CarRunningArcView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public CarRunningArcView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CarRunningArcView); int count = a.getIndexCount(); for (int i = 0; i < count; i++) { int attr = a.getIndex(i); switch (attr) { case R.styleable.CarRunningArcView_arcInsideColor: mArcInsideColor = a.getColor(attr, Color.GRAY); break; case R.styleable.CarRunningArcView_arcInsideStroke: mArcInsideStroke = a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 5, getResources().getDisplayMetrics())); break; case R.styleable.CarRunningArcView_arcOutSideStroke: mArcOutSideStroke = a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 5, getResources().getDisplayMetrics())); break; case R.styleable.CarRunningArcView_radius: mRadius = a.getDimensionPixelSize(attr, (int) TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, 0, getResources().getDisplayMetrics())); break; case R.styleable.CarRunningArcView_sleepTime: mSleepTime = a.getInt(attr, DEFAULT_SLEEP_TIME); break; case R.styleable.CarRunningArcView_ratio: mRatio = a.getFloat(attr, 0); break; default: break; } } a.recycle(); initPaint(); initMipmap(); initOther(); setRatio(mRatio,mCarState); } private void initPaint() { mPaintInside = getPaint(false); mPaintOutside = getPaint(true); } private Paint getPaint(boolean isOutside) { Paint paint = new Paint(); paint.setAntiAlias(true); paint.setStrokeCap(Paint.Cap.ROUND); paint.setStyle(Paint.Style.STROKE); if(isOutside) { paint.setStrokeWidth(mArcOutSideStroke); } else { paint.setStrokeWidth(mArcInsideStroke); paint.setColor(mArcInsideColor); paint.setAlpha(20); } return paint; } private void initOther() { mPath = new Path(); mPathMeasure = new PathMeasure(); mMatrix = new Matrix(); mPos = new float[2]; mTan = new float[2]; } public void setColors(int[] colors) { if(null == colors || 0 == colors.length) { throw new NullPointerException("the color array that you defined must not be null."); } if(colors.length < 2) { throw new IllegalArgumentException("you must specify at least two kinds of colors."); } mArcInsideColors = colors; } private void initMipmap() { mBitmap = BitmapFactory.decodeResource(this.getContext().getResources(), R.mipmap.icon_big_car); } @Override protected void onSizeChanged(int w, int h, int oldW, int oldH) { super.onSizeChanged(w, h, oldW, oldH); mWidth = w; mHeight = h; mCenterX = mWidth / 2; mCenterY = mHeight / 2; if (mRadius + mArcOutSideStroke > Math.min(mWidth / 2, mHeight / 2) || mRadius <= 0) { mRadius = Math.min(mWidth / 2, mHeight / 2) - mArcOutSideStroke; } mRectOval = new RectF(mCenterX - mRadius, mCenterY - mRadius, mCenterX + mRadius, mCenterY + mRadius); mSweepGradient = new SweepGradient(mCenterX, mCenterX, mArcInsideColors, mPositions); mPositions = new float[]{0f,0.5f,0.8f,0.9f}; final float rotationDegrees = mSwipeAngle * 1.0f / 6; mMatrix.setRotate(rotationDegrees, mCenterX, mCenterX); mSweepGradient.setLocalMatrix(mMatrix); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); drawArcInside(canvas); drawArcOutside(canvas); } /** * Draw the arc inside as the progress' background. * * @param canvas */ private void drawArcInside(Canvas canvas) { mPath.reset(); mPath.addArc(mRectOval, mStartAngle, mSwipeAngle); canvas.drawPath(mPath, mPaintInside); } public CarState getCarState() { return mCarState; } public void setCarState(CarState mCarState) { this.mCarState = mCarState; } /** * Set ratio which makes influenced on the arc outside's swipe angle. * * @param ratio */ public void setRatio(double ratio, CarState state) { this.mRatio = ratio; setCarState(state); setExit(false); new Thread(new Runnable() { @Override public void run() { running(); } }).start(); } /** * 如果只是进度条,只要设置进度就可以了,只要开启一个线程 */ /** * Draw the arc outside for current progress. * * @param canvas */ private void drawArcOutside(Canvas canvas) { mPath.reset(); mPath.addArc(mRectOval, mStartAngle, mProgress == 0 ? 0.01f : mProgress); mPathMeasure.setPath(mPath,false); mPathMeasure.getPosTan(mPathMeasure.getLength() * mCurrentValue, mPos, mTan); mBitmapDegrees = (float) (Math.atan2(mTan[1], mTan[0]) * 180.0 / Math.PI); Log.e("JP", "mBitmapDegrees--->" + mBitmapDegrees); if(mCarState == CarState.ROTATING_CLOCKWISE) { mBitmapDegrees += mDegreeClockwise; } if (mCarState ==CarState.MOVING_ANTICLOCKWISE) { mBitmapDegrees += 180; } if(mCarState == CarState.ROTATING_ANTICLOCKWISE) { mBitmapDegrees += mDegreeAnticlockwise; } mMatrix.reset(); mMatrix.postRotate(mBitmapDegrees, mBitmap.getWidth() / 2, mBitmap.getHeight() / 2); mMatrix.postTranslate(mPos[0] - mBitmap.getWidth() / 2, mPos[1] - mBitmap.getHeight() / 2); // 将图片绘制中心调整到与当前点重合 mPaintOutside.setShader(mSweepGradient); canvas.drawPath(mPath, mPaintOutside); canvas.drawBitmap(mBitmap, mMatrix, mPaintOutside); } public boolean isExit() { return exit; } public void setExit(boolean exit) { this.exit = exit; } private void running() { while (!exit) { if(mCarState == CarState.MOVING_CLOCKWISE) {//正在顺时针跑 if (mProgress < mSwipeAngle * mRatio) { movingClockwise(); } else { movedClockwise(); } } if(mCarState == CarState.ROTATING_CLOCKWISE) {//正在顺时针转 if(mDegreeClockwise < 180) { rotatingClockwise(); } else {//顺时针转完毕 rotatedClockwise(); } } if(mCarState == CarState.MOVING_ANTICLOCKWISE) {//正在逆时针跑 if (mProgress > mSwipeAngle * mRatio) { movingAnticlockwise(); } else {//逆时针跑完,如果跑到了0点,则要逆时针转一圈 if(mProgress > 0) { movedAnticlockwise1(); break; } else { movedAnticlockwise2(); } } } if(mCarState == CarState.ROTATING_ANTICLOCKWISE) {//正在逆时针转 if(mDegreeAnticlockwise > 0) { rotatingAnticlockwise(); } else {//逆时针旋转完毕 rotatedAnticlockwise(); } } } } private void rotatingAnticlockwise() { try { Thread.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } mCarState = CarState.ROTATING_ANTICLOCKWISE; mDegreeAnticlockwise--; postInvalidate(); } private void rotatingClockwise() { try { Thread.sleep(5); } catch (InterruptedException e) { e.printStackTrace(); } mCarState = CarState.ROTATING_CLOCKWISE; mDegreeClockwise++; postInvalidate(); } private void movingClockwise() { try { Thread.sleep(mSleepTime); } catch (InterruptedException e) { e.printStackTrace(); } mCarState = CarState.MOVING_CLOCKWISE; mProgress++; setCurrentValue(); postInvalidate(); } private void movedClockwise() { mHandler.postDelayed(new Runnable() { @Override public void run() { if(mRatio > 0) { mCarState = CarState.MOVED_CLOCKWISE;//顺时针跑完毕 mCarState = CarState.ROTATING_CLOCKWISE; setReInitialing(false); } else { mCarState = null; } } },50); } private void movingAnticlockwise() { try { Thread.sleep(mSleepTime); } catch (InterruptedException e) { e.printStackTrace(); } mProgress--; setCurrentValue(); postInvalidate(); } private void movedAnticlockwise1() { mHandler.postDelayed(new Runnable() { @Override public void run() { Log.e("JP", "movedAnticlockwise1"); mCarState = CarState.MOVED_ANTICLOCKWISE; } },50); } private void movedAnticlockwise2() { mHandler.postDelayed(new Runnable() { @Override public void run() { Log.e("JP", "movedAnticlockwise2"); mCarState = CarState.MOVED_ANTICLOCKWISE; mCarState = CarState.ROTATING_ANTICLOCKWISE; } },50); } private void rotatedClockwise() { mHandler.postDelayed(new Runnable() { @Override public void run() { mCarState = CarState.ROTATED_CLOCKWISE; mDegreeClockwise = 0; setExit(true); } },50); } public boolean isReInitialing() { return isReInitialing; } public void setReInitialing(boolean reInitialing) { isReInitialing = reInitialing; } private void rotatedAnticlockwise() { mHandler.postDelayed(new Runnable() { @Override public void run() { if(!isReInitialing) { mCarState = CarState.ROTATED_ANTICLOCKWISE; setExit(true); } else { mCarState = CarState.MOVING_CLOCKWISE; } mDegreeAnticlockwise = 180; } },50); } public int getProgress() { return mProgress; } /** * Set current value([0,1]) */ private void setCurrentValue() { mCurrentValue += mProgress * 1.0 / mSwipeAngle; if (mCurrentValue >= 1) { mCurrentValue = 1; } } /** * Get he ratio. * * @return */ public double getRatio() { return mRatio; } /** * Saved state for arc's progress. */ private static class SavedState extends BaseSavedState { int progress; float bitmapDegrees; float currentValue; double ratio; float[] pos = new float[2]; float[] tan = new float[2]; /** * Constructor called from {@link CarRunningArcView#onSaveInstanceState()} */ SavedState(Parcelable superState) { super(superState); } /** * Constructor called from {@link #CREATOR} */ private SavedState(Parcel in) { super(in); progress = in.readInt(); bitmapDegrees = in.readFloat(); currentValue = in.readFloat(); ratio = in.readDouble(); in.readFloatArray(pos); in.readFloatArray(tan); } @Override public void writeToParcel(Parcel out, int flags) { super.writeToParcel(out, flags); out.writeInt(progress); out.writeFloat(bitmapDegrees); out.writeFloat(currentValue); out.writeDouble(ratio); out.writeFloatArray(pos); out.writeFloatArray(tan); } public static final Creator<SavedState> CREATOR = new Creator<SavedState>() { public SavedState createFromParcel(Parcel in) { return new SavedState(in); } public SavedState[] newArray(int size) { return new SavedState[size]; } }; } @Override public Parcelable onSaveInstanceState() { Parcelable superState = super.onSaveInstanceState(); SavedState ss = new SavedState(superState); ss.progress = mProgress; ss.bitmapDegrees = mBitmapDegrees; ss.currentValue = mCurrentValue; ss.ratio = mRatio; ss.pos = mPos; ss.tan = mTan; return ss; } @Override public void onRestoreInstanceState(Parcelable state) { SavedState ss = (SavedState) state; super.onRestoreInstanceState(ss.getSuperState()); setProgress(ss.progress); mBitmapDegrees = ss.bitmapDegrees; mCurrentValue = ss.currentValue; mRatio = ss.ratio; mPos = ss.pos; mTan = ss.tan; } public void setProgress(int progress) { this.mProgress = progress; } /** * Get sleep time which makes influenced on arc outside's drawing speed. * * @return */ public int getSleepTime() { return mSleepTime; } /** * Set sleep time which makes influence on the arc's drawing speed. * * @param sleepTime */ public void setSleepTime(int sleepTime) { this.mSleepTime = sleepTime; } /** * Get arc inside color. * * @return */ public int getArcInsideColor() { return mArcInsideColor; } /** * Set arc inside color. * * @param arcInsideColor */ public void setArcInsideColor(int arcInsideColor) { this.mArcInsideColor = arcInsideColor; invalidate(); } /** * Get arc inside stroke. * * @return */ public float getArcInsideStroke() { return mArcInsideStroke; } /** * Set arc inside stroke which makes influence on the arc's width. * * @param arcInsideStroke */ public void setArcInsideStroke(float arcInsideStroke) { this.mArcInsideStroke = arcInsideStroke; invalidate(); } /** * Get arc outside's stroke. * * @return */ public float getArcOutSideStroke() { return mArcOutSideStroke; } /** * Set arc outside's stroke which makes influenced on arc's width. * * @param arcOutSideStroke */ public void setArcOutSideStroke(float arcOutSideStroke) { this.mArcOutSideStroke = arcOutSideStroke; invalidate(); } /** * Get arc's radius. * * @return */ public float getRadius() { return mRadius; } /** * Set arc's radius which makes influenced on arc's width and height. * * @param radius */ public void setRadius(float radius) { this.mRadius = radius; } /** * Get the bitmap using for moving forward or back. * @return */ public Bitmap getBitmap() { return mBitmap; } /** * Set the bitmap using for moving forward or back. * @param bitmap */ public void setBitmap(Bitmap bitmap) { if(null == bitmap) { throw new NullPointerException("the bitmap that you specified must not be null."); } this.mBitmap = bitmap; } }